﻿
package extremefx.anim {
	import extremefx.IDisposable;
	import extremefx.anim.transitions.CubicBezierTransition;
	import extremefx.anim.transitions.ITransition;
	import extremefx.events.EventHandler;
	import extremefx.tools.gcLock;	

	/**
	 * @author Marcelo Volmaro
	 */
	public class Tween implements IDisposable {
		public static var defaultEase:ITransition = CubicBezierTransition.EASE_IN_OUT;
		
		/** Setting this to true pauses all tween instances. This does not affect individual tweens' .paused property. **/
		public static var pauseAll:Boolean=false;
		
		private var _target:Object;
		private var _duration:Number;
		private var _position:Number;
		private var _paused:Boolean;
		private var _reversed:Boolean;
		private var _endValues:Object;
		private var _startValues:Object;
		private var _delay:Number;
		private var _inited:Boolean;
		private var _previousPosition:Number;
		private var _previousTweenPosition:Number;
		private var _tweenPosition:Number;
		private var _inTick:Boolean;
		private var _positionOffset:Number;
		private var _maxDistance:Number;
		
		/**
		* The number of times this tween will repeat. If 0, the tween will only run once. If 1 or more, the tween will repeat that many times. If -1, the tween will repeat forever.
		**/
		public var repeat:int;
		
		/**
		* Indicates whether the tween should automatically play when an end value is changed.
		**/
		public var autoPlay:Boolean;
		
		/**
		* Indicates whether the tween should use the reflect mode when repeating. If reflect is set to true, then the tween will play backwards on every other repeat.
		* This has similar effects to reversed, but the properties are exclusive of one another.
		* For instance, with reversed set to false, reflected set to true, and a repeat of 1, the tween will play from start to end, then repeat and reflect to play from end to start.
		* If in the previous example reversed was instead set to true, the tween would play from end to start, then repeat and reflect to play from start to end.
		* Finally, with reversed set to false, reflected set to false, and a repeat of 1, the tween will play from start to end, then repeat from start to end
		**/
		public var reflect:Boolean;
		
		/**
		* Specifies another Tween instance that will have paused=false called on it when this tween completes.
		**/
		public var nextTween:Tween;
		
		
		/**
		* The easing function to use for calculating the tween. This can be any standard tween function, such as the tween functions in fl.motion.easing.* that come with Flash CS3.
		* New tweens will have this set to the defaultTween. Setting this to null will cause Tween to throw null reference errors.
		**/
		public var ease:ITransition;
		
		public var round:Object = {"x":true,"y":true};

		/**
		* Constructs a new Tween instance.
		*
		* @param pTarget The object whose properties will be tweened. Defaults to null.
		* @param pDuration The length of the tween in frames or seconds depending on the timingMode. Defaults to 10.
		* @param pProperties An object containing destination property values. For example, to tween to x=100, y=100, you could pass {x:100, y:100} as the props object.
		**/
		public function Tween(pTarget:Object=null, pDuration:Number=1, pProperties:Object=null) {
			_target = pTarget;
			_duration = pDuration;
			ease = defaultEase || CubicBezierTransition.EASE_IN_OUT;
			
			_position = _tweenPosition = _delay = repeat = _maxDistance = 0;
			_paused = autoPlay = true;
			reflect = false;
			_startValues = {};
			
			setProperties(pProperties);
		}
		
		/**
		* Gets and sets the position in the tween in seconds.
		* <br/><br/>
		* <b>Negative values</b><br/>
		* Values below 0 will always resolve to 0. Negative values can be used to set up a delay on the tween, as the tween will have to count up to 0 before initing.
		* <br/><br/>
		* <b>Positive values</b><br/>
		* Positive values are resolved based on the duration, repeat, reflect, and reversed properties.
		**/
		public function get position():Number {
			return _position;
		}
		
		public function set position(value:Number):void {
			setPosition(value, true);
		}

		/**
		* Indicates whether the tween is currently paused. See play() and pause() for more information.
		**/
		public function get paused():Boolean {
			return _paused;
		}
		
		public function set paused(pValue:Boolean):void {
			if (pValue == _paused) { return; }
			_paused = pValue;
			
			if (pValue) {
				FrameEventDispatcher.onTick.remove(_handleTick);
				
			} else {
				FrameEventDispatcher.onTick.add(_handleTick);
				
				if (repeat != -1 && _position >= _duration * (repeat + 1)) { 
					position = 0;
					 
				} else { 
					_updatePositionOffset(); 
				}
			}
			
			gcLock(this, !pValue);
		}
		
		/**
		* The target object to tween. This can be any kind of object.
		**/
		public function get target():Object {
			return _target;
		}
		
		public function set target(value:Object):void {
			_target = (value === null) ? {} : value;
			_inited = false;
		}
		
		/**
		* Indicates whether a tween should run in reverse. In the simplest examples this means that the tween will play from its end values to its start values.
		* See "reflect" for more information on how these two related properties interact. Also see reverse().
		**/
		public function get reversed():Boolean {
			return _reversed;
		}
		
		public function set reversed(value:Boolean):void {
			if (value == _reversed) return;
			
			_reversed = value;

			if (!_inited) _init();
			
			setPosition(_position, true);
		}
		
		/**
		* The length of the delay in seconds. The delay occurs before a tween reads initial values or starts playing.
		**/
		public function get delay():Number {
			return _delay;
		}
		
		public function set delay(value:Number):void {
			_position = -(_delay = value);
		}
		
		public function setProperties(pProperties:Object):void {
			_endValues = {};
			
			for (var n:String in pProperties) {
				setProperty(n, pProperties[n]);
			}
		}
		
		/**
		* Sets the numeric end value for a property on the target object that you would like to tween.
		* For example, if you wanted to tween to a new x position, you could use: myTween.setProperty("x",400). Non-numeric values are ignored.
		*
		* @param pName The name of the property to tween.
		* @param pValue The numeric end value (the value to tween to).
		**/
		public function setProperty(pName:String, pValue:Number):void {
			if (isNaN(pValue)) return;
			_endValues[pName] = pValue;
			
			if (pName in _startValues){
				_maxDistance = Math.max(Math.abs(_endValues[pName] - _startValues[pName]), _maxDistance); 
			}
			
			_invalidate();
		}
		
		/**
		* Returns the destination value for the specified property if one exists.
		*
		* @param pName The name of the property to return a destination value for.
		**/
		public function getProperty(pName:String):Number {
			return _endValues[pName];
		}
		
		/**
		* Removes a end value from the tween. This prevents the Tween instance from tweening the property.
		*
		* @param pName The name of the end property to delete.
		**/
		public function deleteProperty(pName:String):Boolean {
			return delete(_endValues[pName]);
		}
		
		/**
		* Invalidate forces the tween to repopulate all of the initial properties from the target object, and start playing if autoplay is set to true.
		* If the tween is currently playing, then it will also set the position to 0. For example, if you changed the x and y position of a the target
		* object while the tween was playing, you could call invalidate on it to force it to resume the tween with the new property values.
		**/
		private function _invalidate():void {
			_inited = false;
			
			if (_position > 0) {
				_position = 0;
				_updatePositionOffset();
			}
			
			if (autoPlay) paused = false;
		}
		
		/**
		* Jumps the tween to its beginning. This is the same as setting <code>position=-delay</code>.
		**/
		public function beginning():void {
			setPosition(-_delay);
		}
		
		/**
		* Jumps the tween to its end. This is the same as setting <code>position=(repeat+1)*duration</code>.
		**/
		public function end():void {
			setPosition( (repeat == -1) ? _duration : (repeat + 1) * _duration);
		}
		
		/**
		* Sets the position of the tween. Using the position property will always suppress events and callbacks, whereas the
		* setPosition method allows you to manually set the position and specify whether to suppress events or not.
		*
		* @param pPosition The position to jump to in seconds.
		* @param pSuppressEvents Indicates whether to suppress events and callbacks generated from the change in position.
		**/
		public function setPosition(pPosition:Number, pSuppressEvents:Boolean=true):void {
			_previousPosition = _position;
			_position = pPosition;
			
			if (!_inTick && !paused) { 
				_updatePositionOffset(); 
			}
			
			var maxPos:Number = (repeat + 1) * _duration;
			var tp:Number;
			
			if (pPosition < 0) {
				tp = _reversed ? _duration : 0;
				
			} else if (repeat == -1 || pPosition < maxPos) {
				tp = pPosition % _duration;
				
				if ((reflect && pPosition / _duration % 2 >= 1) != _reversed) { 
					tp = _duration - tp; 
				}
				
			} else {
				tp = ((reflect && repeat % 2 >= 1) != _reversed) ? 0 : _duration;
			}
			
			if (tp == _tweenPosition) return;
			
			_previousTweenPosition = _tweenPosition;
			_tweenPosition = tp;
			
			if (!pSuppressEvents && _eOnChange) {
				_eOnChange.fire();
			}
			
			if (!_inited && _previousPosition <= 0 && _position >= 0) {
				_init();
				if (!pSuppressEvents && _eOnInit)  _eOnInit.fire();
			}
			
			//max distance is the maximum distance between two properties this tween has. Some transitions
			//may use this number to provide smooth results (like the Cubic Bezier transition).
			var ratio:Number = ease.step(_tweenPosition/_duration, 0, _maxDistance, 1);
			
			for (var n:String in _endValues) {
				var value:Number = _startValues[n] + (_endValues[n] - _startValues[n]) * ratio;
				
				if (n == "currentFrame"){
					_target.gotoAndStop(value << 0);
						
				} else {
					if (n in round){
						value = Math.round(value);
					}
					
					_target[n] = value;
				} 
			}
			
//			if ("alpha" in _endValues && "alpha" in _target && "visible" in _target) { _target.visible = _target.alpha > 0; }
			
			if (repeat != -1 && _previousPosition < maxPos && pPosition >= maxPos) {
				if (!pSuppressEvents && _eOnComplete) _eOnComplete.fire();
				paused = true;
				if (nextTween) { nextTween.paused = false; }
			}
		}
		
		/**
		 * @private
		 * copies the initial target properties into the local startValues store. 
		 */
		private function _init():void {
			_inited = true;
			_startValues = {};
			
			for (var n:String in _endValues) {
				_startValues[n] = _target[n];
				_maxDistance = Math.max(Math.abs(_endValues[n] - _startValues[n]), _maxDistance);
			}
		}
		
		// updates the current positionOffset based on the current ticker position.
		private function _updatePositionOffset():void {
			_positionOffset = FrameEventDispatcher.position - _position;
		}
		
		// handles tick events while playing.
		private function _handleTick(pSender:FrameEventDispatcher, evt:TickEventArgs):void {
			_inTick = true;
			
			if (pauseAll) { 
				_updatePositionOffset();
				
			} else { 
				setPosition(evt.position - _positionOffset, false); 
			}
			
			_inTick = false;
		}
		
		private var _eOnComplete:EventHandler;
		public function get onComplete():EventHandler{
			if (_eOnComplete == null) _eOnComplete = new EventHandler(this);
			return _eOnComplete;
		}
		
		private var _eOnInit:EventHandler;
		public function get onInit():EventHandler{
			if (_eOnInit == null) _eOnInit = new EventHandler(this);
			return _eOnInit;
		}
		
		private var _eOnChange:EventHandler;
		public function get onChange():EventHandler{
			if (_eOnChange == null) _eOnChange = new EventHandler(this);
			return _eOnChange;
		}
		
		public function dispose():void {
			FrameEventDispatcher.onTick.remove(_handleTick);
			
			if (_eOnComplete) _eOnComplete.dispose();
			if (_eOnInit) _eOnInit.dispose();
			if (_eOnChange) _eOnChange.dispose();

			gcLock(this, false);
		}
		
		public static function animate(pTarget:Object, pDuration:Number=1):Tween {
			var t:Tween = new Tween(pTarget, pDuration);
			t.onComplete.add(function(...pArgs):void{t.dispose();});
			return t;
		}
	}
}